Skip to content

feat: partial-success batch insert + exact import-retry idempotency (#3172, #3173)#3191

Merged
os-zhuang merged 3 commits into
mainfrom
claude/evaluate-issues-3147-5152-1naxe6
Jul 18, 2026
Merged

feat: partial-success batch insert + exact import-retry idempotency (#3172, #3173)#3191
os-zhuang merged 3 commits into
mainfrom
claude/evaluate-issues-3147-5152-1naxe6

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

Follow-ups to the #3161 bulk-write hardening, closing the two gaps it left documented. Two commits, one per issue.

Fixes #3172, #3173.

#3172 — partial-success batch insert (root fix for the #3152 leftover)

A batch that died in validation forced bulkWrite's whole-batch degradation, which re-ran the entire engine.insert — firing beforeInsert hooks a second time on the good rows.

  • engine.insertMany(object, rows, options) (new, objectql): bad rows are culled per-row after beforeInsert (encryption / validation / autonumber failures each captured per row), survivors are written in one driver batch, and the return is one outcome per input row in input order ({ok, record} / {ok: false, error}). afterInsert, summary recompute, and realtime fire only for written rows; dead rows consume no autonumber sequence value. Plain insert(array) semantics are untouched (still all-or-abort), so no existing caller changes behavior. Implemented as a flag inside the existing batch path — no code duplication.
  • bulkWrite.writeBatchPartial (optional, core): per-row failures from it are final verdicts — no writeOne degradation for them (that would re-fire the hooks). Only a thrown error (transient, ERR_BULK_RESULT_MISMATCH) still degrades, with the same retry semantics as writeBatch.
  • Adoption: seed loader's flushPendingInserts and the import runner (via the protocol's new insertManyData, same readonly ingress strip as createManyData) switch over with feature detection — a legacy engine/protocol keeps the previous whole-array + degradation behavior.
  • SummaryRecomputeError (bug(objectql): 汇总重算失败仅 warn——子记录批量插入报成功,父 summary 静默过期且无重试 #3147) composes: in partial mode its written payload is the outcome array, and the existing seed/import recovery handles it unchanged.

Acceptance (pinned by tests): the #3152 probe [{good1},{no-name},{good2}] now runs beforeInsert exactly once per row, afterInsert only for written rows, autonumbers stay contiguous.

#3173 — exact import-retry idempotency via pre-assigned row ids

Pure-insert imports (no matchFields) had no natural key to recheck on a commit-then-lost-response retry, so #3161 left them documented as at-least-once (a retry duplicated the batch).

  • Every buffered CREATE row gets a client-generated id at flush time (stable across retry attempts; an explicit user-supplied id is respected). Both the SQL driver's create/bulkCreate honor caller-provided ids, and the protocol's readonly strip does not touch id, so this needs no schema migration.
  • On attempt > 1 (or after an uncertain batch outcome) the runner rechecks by id $in and re-inserts only rows that truly did not land. This replaces the previous matchFields approximation and is exact for every write mode — including files with legitimate duplicate rows in insert mode, which no natural-key or fingerprint recheck could distinguish (each copy has its own id).
  • The design alternatives from the issue (system sys_import_job_id column, job-side accounting, created_at-window fingerprinting) were evaluated and rejected: the column needs a migration; fingerprinting has a real data-loss edge for back-to-back identical imports plus DB-vs-app clock skew on Postgres/MySQL.
  • The previously pinned at-least-once test is flipped: pure-insert retry after commit now expects zero duplicates.

Tests

Verification

  • pnpm build — 71/71 (type-checks across all packages).
  • pnpm test --concurrency=2131/131 task suites pass. (Unbounded concurrency OOM-kills random vitest workers in this container; the failures reproduced on untouched packages and each passed standalone.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01APnSDJneEDRh3Z51KGCLga


Generated by Claude Code

claude added 2 commits July 18, 2026 06:56
… fires once (#3172)

Root fix for the #3152 leftover: a batch that died in validation forced
bulkWrite's whole-batch degradation, re-running beforeInsert hooks on the
good rows (at-least-once side effects).

- engine.insertMany(object, rows, options): new partial-success entry.
  Bad rows are culled per-row AFTER beforeInsert (encryption/validation/
  autonumber failures each captured per row), survivors are written in
  one driver batch, and the return is one outcome per input row in input
  order ({ok,record} | {ok:false,error}). afterInsert, summary recompute
  and realtime fire only for written rows; dead rows consume no
  autonumber. Plain insert(array) semantics are untouched.
- bulkWrite gains an optional writeBatchPartial: per-row failures from it
  are final verdicts (no writeOne degradation — that would re-fire the
  hooks); only a THROWN error (transient, mismatch) still degrades.
- seed-loader's flushPendingInserts switches to writeBatchPartial and
  feature-detects engine.insertMany (legacy engines keep the old
  whole-array + degradation behavior).
- protocol.insertManyData: partial-success sibling of createManyData
  (same readonly ingress strip), for the import runner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APnSDJneEDRh3Z51KGCLga
…adopt partial-success create (#3173, #3172)

Pure-insert imports (no matchFields) had no natural key to recheck on a
commit-then-lost-response retry, so they stayed at-least-once — a retry
duplicated the whole batch (#3149 left this documented, #3173 tracked
closing it).

Every buffered CREATE row now gets a client-generated id at flush time
(stable across attempts; an explicit user-supplied id is respected — both
the SQL driver and bulkCreate honor caller ids). The retry path rechecks
by id ($in) and re-inserts only rows that truly did not land. This is
exact for every write mode: legitimate duplicate rows in insert mode
resolve correctly because each copy has its own id — the case a
natural-key or fingerprint recheck cannot distinguish. The old
matchFields-based recheck is replaced by the id recheck.

The import runner also prefers the protocol's new insertManyData
(framework#3172) via bulkWrite's writeBatchPartial: a row that fails
validation is a per-row verdict from one call, so a bad row no longer
forces the whole-batch degradation that re-ran beforeInsert hooks on its
siblings. Protocols without insertManyData keep the createManyData path.

Flips the previously pinned at-least-once test: pure-insert retry after
commit now expects zero duplicates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APnSDJneEDRh3Z51KGCLga
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 18, 2026 8:19am

Request Review

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/core, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/rest, @objectstack/runtime.

39 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/automation/webhooks.mdx (via @objectstack/core)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, @objectstack/objectql)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/core)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/rest, @objectstack/runtime)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang force-pushed the claude/evaluate-issues-3147-5152-1naxe6 branch from 0147d3c to c2367ab Compare July 18, 2026 07:41
…#3147#3152, #3172, #3173)

The 2026-07-06 incident's root lesson: the in-memory mock driver masked
the bug (stored real booleans, never threw a transient error), so
faithful local repro stayed green while turso dropped rows. Every probe
so far uses a mock — this wires the REAL ObjectQL engine to the REAL
SqlDriver (better-sqlite3, on-disk) and injects turso's actual failure
shapes through a thin driver proxy:

- #3172 insertMany culls a bad row per-row and writes survivors with a
  contiguous persistent autonumber sequence.
- #3147 a transient blip on the parent summary UPDATE is retried; the
  roll-up recomputes correctly over a real SQL aggregate.
- #3149/#3173 a bulkCreate that commits then loses its response (the
  exact turso shape the mock can't produce) does NOT duplicate seed rows
  — the natural-key recheck runs against the real table.
- #3150 a transient blip on the self-referencing sequential seed path is
  retried, not dropped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APnSDJneEDRh3Z51KGCLga
@os-zhuang
os-zhuang force-pushed the claude/evaluate-issues-3147-5152-1naxe6 branch from c2367ab to e25808d Compare July 18, 2026 07:48
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 08:05
@os-zhuang
os-zhuang merged commit 621b2cc into main Jul 18, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the claude/evaluate-issues-3147-5152-1naxe6 branch July 18, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql: 批插入部分成功 API——根治降级重写下 beforeInsert 钩子 at-least-once(#3152 跟进)

2 participants